/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2011  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu.graph;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;

import org.sosy_lab.ccvisu.graph.GraphVertex.Shape;
import org.sosy_lab.ccvisu.graph.Group.GroupKind;
import org.sosy_lab.ccvisu.graph.interfaces.GraphEventListener;
import org.sosy_lab.common.LogManager;
import org.sosy_lab.util.Colors;

/**
 * Contains the representation of a graph.
 * This class is a collection of the data structures needed
 * for layout and transformation processing.
 */
public class GraphData {

  /** Maps a vertex id to a GraphVertex.*/
  private List<GraphVertex>                             vertices                = new ArrayList<GraphVertex>();

  /** Maps a vertex name to a GraphVertex. */
  private Map<String, GraphVertex>                      nameToVertex            = new HashMap<String, GraphVertex>();

  /** Edges of type GraphEdgeInt. Only used if (inFormat < LAY). */
  private List<GraphEdge>                               edges                   = new ArrayList<GraphEdge>();

  /** Raw RSF data as extracted from input. */
  private Relation                                      tuples                  = new Relation();

  /** A cache data structure for getAdjacent. */
  private Map<String, Map<GraphVertex, Set<GraphEdge>>> adjacentCache           = null;

  /** A cache data structure for getAdjacentUndirected. */
  private Map<String, Map<GraphVertex, Set<GraphEdge>>> adjacentCacheUndirected = null;

  private float                                         degreeMax               = 0;
  private float                                         degreeOutMax            = 0;

  // List of groups.
  private List<Group>                                   groups                  = new ArrayList<Group>();
  private Group                                         defaultGroup;

  /** contain the name of node's group */
  private String[]                                      indexToGroupName        = null;

  /** Notify these observers, when changes occur in the graph */
  private Collection<GraphEventListener>                onGraphChangeListeners  = new ArrayList<GraphEventListener>();

  private final LogManager                              logger;

  public GraphData(LogManager logger) {
    this.logger = logger;
  }

  // TODO: javadoc is missing
  public GraphVertex getVertexById(int id) {
    for (GraphVertex vertex : vertices) {
      if (id == vertex.getId()) {
        return vertex;
      }
    }

    // found no vertex with this id
    return null;
  }

  // TODO: javadoc is missing
  public GraphVertex getVertexByName(String vertexName) {
    return nameToVertex.get(vertexName);
  }

  // TODO: javadoc is missing
  public List<GraphVertex> getVertices() {
    return Collections.unmodifiableList(vertices);
  }

  // TODO: javadoc is missing
  public void setVertices(List<GraphVertex> vertices) {
    this.vertices = vertices;

    // Create the reverse lookup (nameToVertex).
    for (int i = 0; i < vertices.size(); ++i) {
      GraphVertex graphVertex = vertices.get(i);
      graphVertex.setId(i);

      // Add vertex-to-number entry for vertex.
      if (nameToVertex.containsKey(graphVertex.getName())) {
        logger.log(Level.WARNING, "Input error: Vertex '" + graphVertex.getName()
            + "' exists twice in given input.");
      }
      nameToVertex.put(graphVertex.getName(), graphVertex);
    }
    // Initialize groups.
    initGroups();
  }

  /**
   * Initializes the  group representation (of the layout).
   */
  private void initGroups() {
    //create default group with all the nodes in it
    indexToGroupName = new String[vertices.size()];
    if (defaultGroup == null) {
      defaultGroup = new Group("Group 'Unassigned'", Colors.GREEN.get(), Shape.DISC, this);
    }

    groups.clear();
    addGroup(defaultGroup);
    for (GraphVertex vertex : vertices) {
      defaultGroup.addNode_WO_COLOR(vertex);
    }

    //edges annotation
    for (GraphEdge edge : edges) {
      edge.setShowName(false);
    }
  }

  public GraphEdge getEdgeById(int id) {
    for (GraphEdge edge : edges) {
      if (id == edge.getId()) {
        return edge;
      }
    }

    // found no edge with this id
    return null;
  }

  /**
   * @return the edges
   */
  public List<GraphEdge> getEdges() {
    return edges;
  }

  // TODO: javadoc is missing
  public void addEdge(GraphEdge edge) {
    GraphVertex source = edge.getSource();
    GraphVertex target = edge.getTarget();

    source.setDegree(source.getDegree() + edge.getWeight());
    target.setDegree(target.getDegree() + edge.getWeight());
    source.setDegreeOut(source.getDegreeOut() + edge.getWeight());

    edge.setId(edges.size());

    degreeMax = Math.max(degreeMax, source.getDegree());
    degreeOutMax = Math.max(degreeOutMax, source.getDegreeOut());
    edges.add(edge);
  }

  /**
   * @return the Relation associated with this graph.
   */
  public Relation getRelation() {
    return tuples;
  }

  // TODO: javadoc is missing
  public void setRelation(Relation relation) {
    // TODO: apply relation to graph here
    tuples = relation;
  }

  // TODO: javadoc is missing
  public float getMaxDegree() {
    return degreeMax;
  }

  // TODO: javadoc is missing
  public float getMaxOutdegree() {
    return degreeOutMax;
  }

  /**
   * Returns the set of adjacent edges
   * for a given vertex and a given relation.
   */
  public Collection<GraphEdge> getAdjacent(GraphVertex graphVertex, String relationName) {

    // Initialize the map.
    if (adjacentCache == null) {
      adjacentCache = new TreeMap<String, Map<GraphVertex, Set<GraphEdge>>>();
    }

    // Look-up the relation name.
    Map<GraphVertex, Set<GraphEdge>> vertexMap = adjacentCache.get(relationName);
    if (vertexMap == null) {
      vertexMap = new HashMap<GraphVertex, Set<GraphEdge>>();
      adjacentCache.put(relationName, vertexMap);
    }

    // Look-up the vertex.
    Set<GraphEdge> edgeSet = vertexMap.get(graphVertex);
    if (edgeSet == null) {
      edgeSet = new TreeSet<GraphEdge>();
      vertexMap.put(graphVertex, edgeSet);
      for (GraphEdge graphEdge : edges) {
        try {
          if (graphEdge.getSource() == graphVertex && graphEdge.getRelationName().matches(relationName)) {
            edgeSet.add(graphEdge);
          }
        } catch (Exception pException) {
          System.err.println("Exception in RegExp match: " + pException.getMessage());
        }
      }
    }
    return edgeSet;
  }

  /**
   * Returns the set of adjacent edges (incoming and outgoing)
   * for a given vertex and a given relation.
   */
  public Collection<GraphEdge> getAdjacentUndirected(GraphVertex graphVertex, String relationName) {

    // Initialize the map.
    if (adjacentCacheUndirected == null) {
      adjacentCacheUndirected = new TreeMap<String, Map<GraphVertex, Set<GraphEdge>>>();
    }

    // Look-up the relation name.
    Map<GraphVertex, Set<GraphEdge>> vertexMap = adjacentCacheUndirected.get(relationName);
    if (vertexMap == null) {
      vertexMap = new HashMap<GraphVertex, Set<GraphEdge>>();
      adjacentCacheUndirected.put(relationName, vertexMap);
    }

    // Look-up the vertex.
    Set<GraphEdge> edgeSet = vertexMap.get(graphVertex);
    if (edgeSet == null) {
      edgeSet = new TreeSet<GraphEdge>();
      vertexMap.put(graphVertex, edgeSet);
      for (GraphEdge graphEdge : edges) {
        try {
          if ((graphEdge.getSource() == graphVertex || graphEdge.getTarget() == graphVertex)
              && graphEdge.getRelationName().matches(relationName)) {

            edgeSet.add(graphEdge);
          }
        } catch (Exception pException) {
          System.err.println("Exception in RegExp match: " + pException.getMessage());
        }
      }
    }
    return edgeSet;
  }

  /**
   * return the group at the specified index
   * @param index
   * @return the group at the specified index
   */
  public Group getGroup(int index) {
    if (index >= 0 && index < groups.size()) {
      return groups.get(index);
    } else {
      return null;
    }
  }

  /**
   * return the group with the specified name
   * @param name
   * @return the group with the specified name
   */
  public Group getGroup(String name) {
    for (Group group : groups) {
      if (group.getName().equals(name)) {
        return group;
      }
    }
    return null;
  }

  // TODO: javadoc is missing
  public Group getDefaultGroup() {
    return defaultGroup;
  }

  // TODO: javadoc is missing
  public List<Group> getGroups() {
    return groups;
  }

  /**
   * @return the names of the node group
   */
  String[] getIndexToGroupName() {
    return indexToGroupName;
  }

  /**
   * get the number of groups
   * @return return the number of groups
   */
  public int getNumberOfGroups() {
    return groups.size();
  }

  /**
   * add a new group in the list
   * @param group
   */
  public void addGroup(Group group) {
    groups.add(0, group);
    notifyAboutGroupsChange(new EventObject(this));
  }

  /**
   * remove the group at the specified index
   * @param index
   */
  public void removeGroup(int index) {
    assert (index != 0);
    Group group = (Group) groups.get(index);
    Group defaultGroup = (Group) groups.get(0);

    for (GraphVertex vertex : group) {
      defaultGroup.addNode(vertex);
    }

    groups.remove(index);
    notifyAboutGroupsChange(new EventObject(this));
  }

  /**
   * move the group at index one place higher in the list
   * => group drawn sooner
   * @param index
   */
  public void moveGroupUp(int index) {
    if (index > 1) {
      Group tmp = (Group) groups.get(index);
      groups.remove(index);
      groups.add(index - 1, tmp);
    }

    notifyAboutGroupsChange(new EventObject(this));
  }

  /**
   * move the group at index one place lower in the list
   * => drawn later (more on top)
   * @param index
   */
  public void moveGroupDown(int index) {
    if (index < groups.size() - 1 && index > 0) {
      Group tmp = (Group) groups.get(index);
      groups.remove(index);
      groups.add(index + 1, tmp);
    }

    notifyAboutGroupsChange(new EventObject(this));
  }

  public boolean isEdgesAvailable() {
    return !getEdges().isEmpty();
  }

  /**
   * Remove all clustering groups.
   */
  public void clearClustering() {
    Iterator<Group> iterator = groups.iterator();
    while(iterator.hasNext()) {
      if (iterator.next().getKind() == GroupKind.CLUSTER) {
        iterator.remove();
      }
    }
  }

  /**
   * Get number of clusters.
   */
  public int getNumberOfClusters() {
    int result = 0;
    for (Group group : groups) {
      if (group.getKind() == GroupKind.CLUSTER) {
        result++;
      }
    }
    return result;
  }

  /**
   * Add a observer to the graph-change-event.
   * @param observer
   */
  public void addOnGraphChangeListener(GraphEventListener listener) {
    onGraphChangeListeners.add(listener);
  }

  /**
   * Add a observer to the group-change-event.
   * @param observer
   */
  public void addOnGroupChangeListener(GraphEventListener listener) {
    // TODO: Use own listener.
    onGraphChangeListeners.add(listener);
  }

  /**
   * Notify all listeners about a change in the graph layout.
   */
  public void notifyAboutLayoutChange(EventObject event) {
    // notify the listeners about the layout change.
    for (GraphEventListener listener : onGraphChangeListeners) {
      try {
        listener.onGraphChangedEvent(event);
      } catch (Exception e) {
        // Maybe the listener (i.e., the drawing frame) was closed.
        e.printStackTrace();
        System.exit(0);
      }
    }

    // the groups my have changed too??
    // TODO: Really required?
    refreshGroups();
  }

  /**
   * tells the group that the graph has changed => recompute some data
   */
  private void refreshGroups() {
    for (Group group : groups) {
      group.graphchanged();
    }
  }

  /**
   * Notify all listeners about a change in the groups.
   */
  public void notifyAboutGroupsChange(EventObject event) {
    // TODO: Use own listener list.
    for (GraphEventListener listener : onGraphChangeListeners) {
      try {
        listener.onGraphChangedEvent(event);
      } catch (Exception e) {
        // Maybe the listener (i.e., the drawing frame) was closed.
        e.printStackTrace();
        System.exit(0);
      }
    }
  }
}
